_.extend._onModelEvent   C
last analyzed

Complexity

Conditions 10
Paths 14

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
c 1
b 0
f 0
nc 14
nop 4
dl 0
loc 24
rs 5.2164

How to fix   Complexity   

Complexity

Complex classes like _.extend._onModelEvent often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import $ from 'jquery';
0 ignored issues
show
introduced by
Definition for rule 'keyword-spacing' was not found
Loading history...
2
import _ from 'underscore';
3
import {
4
  Backbone,
5
  wrapError,
6
  addUnderscoreMethods
7
} from './core.js';
8
import {
9
  Events
10
} from './events.js';
11
import {
12
  Model
13
} from './model.js';
14
15
// Create a local reference to a common array method we'll want to use later.
16
var slice = Array.prototype.slice;
17
18
// Backbone.Collection
19
// -------------------
20
21
// If models tend to represent a single row of data, a Backbone Collection is
22
// more analogous to a table full of data ... or a small slice or page of that
23
// table, or a collection of rows that belong together for a particular reason
24
// -- all of the messages in this particular folder, all of the documents
25
// belonging to this particular author, and so on. Collections maintain
26
// indexes of their models, both in order, and for lookup by `id`.
27
28
// Create a new **Collection**, perhaps to contain a specific type of `model`.
29
// If a `comparator` is specified, the Collection will maintain
30
// its models in sort order, as they're added and removed.
31
var Collection = function (models, options) {
32
  options = options || {};
33
  this.preinitialize.apply(this, arguments);
34
  if (options.model) {
35
    this.model = options.model;
36
  }
37
  if (options.comparator !== void 0) {
38
    this.comparator = options.comparator;
39
  }
40
  this._reset();
41
  this.initialize.apply(this, arguments);
42
  if (models) {
43
    this.reset(models, _.extend({
44
      silent: true
45
    }, options));
46
  }
47
};
48
49
// Default options for `Collection#set`.
50
var setOptions = {
51
  add: true,
52
  remove: true,
53
  merge: true
54
};
55
var addOptions = {
56
  add: true,
57
  remove: false
58
};
59
60
// Splices `insert` into `array` at index `at`.
61
var splice = function (array, insert, at) {
62
  at = Math.min(Math.max(at, 0), array.length);
63
  var tail = Array(array.length - at);
64
  var length = insert.length;
65
  var i;
66
  for (i = 0; i < tail.length; i++) {
67
    tail[i] = array[i + at];
68
  }
69
  for (i = 0; i < length; i++) {
70
    array[i + at] = insert[i];
71
  }
72
  for (i = 0; i < tail.length; i++) {
73
    array[i + length + at] = tail[i];
74
  }
75
};
76
77
// Define the Collection's inheritable methods.
78
_.extend(Collection.prototype, Events, {
79
80
  // The default model for a collection is just a **Backbone.Model**.
81
  // This should be overridden in most cases.
82
  model: Model,
83
84
  // preinitialize is an empty function by default. You can override it with a function
85
  // or object.  preinitialize will run before any instantiation logic is run in the Collection.
86
  preinitialize: function () {},
87
88
  // Initialize is an empty function by default. Override it with your own
89
  // initialization logic.
90
  initialize: function () {},
91
92
  // The JSON representation of a Collection is an array of the
93
  // models' attributes.
94
  toJSON: function (options) {
95
    return this.map(function (model) {
96
      return model.toJSON(options);
97
    });
98
  },
99
100
  // Proxy `Backbone.sync` by default.
101
  sync: function () {
102
    return Backbone.sync.apply(this, arguments);
103
  },
104
105
  // Add a model, or list of models to the set. `models` may be Backbone
106
  // Models or raw JavaScript objects to be converted to Models, or any
107
  // combination of the two.
108
  add: function (models, options) {
109
    return this.set(models, _.extend({
110
      merge: false
111
    }, options, addOptions));
112
  },
113
114
  // Remove a model, or a list of models from the set.
115
  remove: function (models, options) {
116
    options = _.extend({}, options);
117
    var singular = !_.isArray(models);
118
    models = singular ? [models] : models.slice();
119
    var removed = this._removeModels(models, options);
120
    if (!options.silent && removed.length) {
121
      options.changes = {
122
        added: [],
123
        merged: [],
124
        removed: removed
125
      };
126
      this.trigger('update', this, options);
127
    }
128
    return singular ? removed[0] : removed;
129
  },
130
131
  // Update a collection by `set`-ing a new list of models, adding new ones,
132
  // removing models that are no longer present, and merging models that
133
  // already exist in the collection, as necessary. Similar to **Model#set**,
134
  // the core operation for updating the data contained by the collection.
135
  set: function (models, options) {
136
    if (models == null) {
137
      return;
138
    }
139
140
    options = _.extend({}, setOptions, options);
141
    if (options.parse && !this._isModel(models)) {
142
      models = this.parse(models, options) || [];
143
    }
144
145
    var singular = !_.isArray(models);
146
    models = singular ? [models] : models.slice();
147
148
    var at = options.at;
149
    if (at != null) {
150
      at = +at;
151
    }
152
    if (at > this.length) {
153
      at = this.length;
154
    }
155
    if (at < 0) {
156
      at += this.length + 1;
157
    }
158
159
    var set = [];
160
    var toAdd = [];
161
    var toMerge = [];
162
    var toRemove = [];
163
    var modelMap = {};
164
165
    var add = options.add;
166
    var merge = options.merge;
167
    var remove = options.remove;
168
169
    var sort = false;
170
    var sortable = this.comparator && at == null && options.sort !==
171
      false;
172
    var sortAttr = _.isString(this.comparator) ? this.comparator :
173
      null;
174
175
    // Turn bare objects into model references, and prevent invalid models
176
    // from being added.
177
    var model, i;
178
    for (i = 0; i < models.length; i++) {
179
      model = models[i];
180
181
      // If a duplicate is found, prevent it from being added and
182
      // optionally merge it into the existing model.
183
      var existing = this.get(model);
184
      if (existing) {
185
        if (merge && model !== existing) {
186
          var attrs = this._isModel(model) ? model.attributes :
187
            model;
188
          if (options.parse) {
0 ignored issues
show
introduced by
Blocks are nested too deeply (4).
Loading history...
189
            attrs = existing.parse(attrs,
190
              options);
191
          }
192
          existing.set(attrs, options);
193
          toMerge.push(existing);
194
          if (sortable && !sort) {
0 ignored issues
show
introduced by
Blocks are nested too deeply (4).
Loading history...
195
            sort = existing.hasChanged(
196
              sortAttr);
197
          }
198
        }
199
        if (!modelMap[existing.cid]) {
200
          modelMap[existing.cid] = true;
201
          set.push(existing);
202
        }
203
        models[i] = existing;
204
205
        // If this is a new, valid model, push it to the `toAdd` list.
206
      } else if (add) {
207
        model = models[i] = this._prepareModel(model, options);
208
        if (model) {
209
          toAdd.push(model);
210
          this._addReference(model, options);
211
          modelMap[model.cid] = true;
212
          set.push(model);
213
        }
214
      }
215
    }
216
217
    // Remove stale models.
218
    if (remove) {
219
      for (i = 0; i < this.length; i++) {
220
        model = this.models[i];
221
        if (!modelMap[model.cid]) {
222
          toRemove.push(model);
223
        }
224
      }
225
      if (toRemove.length) {
226
        this._removeModels(toRemove, options);
227
      }
228
    }
229
230
    // See if sorting is needed, update `length` and splice in new models.
231
    var orderChanged = false;
232
    var replace = !sortable && add && remove;
233
    if (set.length && replace) {
234
      orderChanged = this.length !== set.length || _.some(this.models,
235
        function (m, index) {
236
          return m !== set[index];
237
        });
238
      this.models.length = 0;
239
      splice(this.models, set, 0);
240
      this.length = this.models.length;
241
    } else if (toAdd.length) {
242
      if (sortable) {
243
        sort = true;
244
      }
245
      splice(this.models, toAdd, at == null ? this.length : at);
246
      this.length = this.models.length;
247
    }
248
249
    // Silently sort the collection if appropriate.
250
    if (sort) {
251
      this.sort({
252
        silent: true
253
      });
254
    }
255
256
    // Unless silenced, it's time to fire all appropriate add/sort/update events.
257
    if (!options.silent) {
258
      for (i = 0; i < toAdd.length; i++) {
259
        if (at != null) {
260
          options.index = at + i;
261
        }
262
        model = toAdd[i];
263
        model.trigger('add', model, this, options);
264
      }
265
      if (sort || orderChanged) {
266
        this.trigger('sort', this,
267
          options);
268
      }
269
      if (toAdd.length || toRemove.length || toMerge.length) {
270
        options.changes = {
271
          added: toAdd,
272
          removed: toRemove,
273
          merged: toMerge
274
        };
275
        this.trigger('update', this, options);
276
      }
277
    }
278
279
    // Return the added (or merged) model (or models).
280
    return singular ? models[0] : models;
281
  },
282
283
  // When you have more items than you want to add or remove individually,
284
  // you can reset the entire set with a new list of models, without firing
285
  // any granular `add` or `remove` events. Fires `reset` when finished.
286
  // Useful for bulk operations and optimizations.
287
  reset: function (models, options) {
288
    options = options ? _.clone(options) : {};
289
    for (var i = 0; i < this.models.length; i++) {
290
      this._removeReference(this.models[i], options);
291
    }
292
    options.previousModels = this.models;
293
    this._reset();
294
    models = this.add(models, _.extend({
295
      silent: true
296
    }, options));
297
    if (!options.silent) {
298
      this.trigger('reset', this, options);
299
    }
300
    return models;
301
  },
302
303
  // Add a model to the end of the collection.
304
  push: function (model, options) {
305
    return this.add(model, _.extend({
306
      at: this.length
307
    }, options));
308
  },
309
310
  // Remove a model from the end of the collection.
311
  pop: function (options) {
312
    var model = this.at(this.length - 1);
313
    return this.remove(model, options);
314
  },
315
316
  // Add a model to the beginning of the collection.
317
  unshift: function (model, options) {
318
    return this.add(model, _.extend({
319
      at: 0
320
    }, options));
321
  },
322
323
  // Remove a model from the beginning of the collection.
324
  shift: function (options) {
325
    var model = this.at(0);
326
    return this.remove(model, options);
327
  },
328
329
  // Slice out a sub-array of models from the collection.
330
  slice: function () {
331
    return slice.apply(this.models, arguments);
332
  },
333
334
  // Get a model from the set by id, cid, model object with id or cid
335
  // properties, or an attributes object that is transformed through modelId.
336
  get: function (obj) {
337
    if (obj == null) {
338
      return void 0;
339
    }
340
    return this._byId[obj] ||
341
      this._byId[this.modelId(obj.attributes || obj)] ||
342
      obj.cid && this._byId[obj.cid];
343
  },
344
345
  // Returns `true` if the model is in the collection.
346
  has: function (obj) {
347
    return this.get(obj) != null;
348
  },
349
350
  // Get the model at the given index.
351
  at: function (index) {
352
    if (index < 0) {
353
      index += this.length;
354
    }
355
    return this.models[index];
356
  },
357
358
  // Return models with matching attributes. Useful for simple cases of
359
  // `filter`.
360
  where: function (attrs, first) {
361
    return this[first ? 'find' : 'filter'](attrs);
362
  },
363
364
  // Return the first model with matching attributes. Useful for simple cases
365
  // of `find`.
366
  findWhere: function (attrs) {
367
    return this.where(attrs, true);
368
  },
369
370
  // Force the collection to re-sort itself. You don't need to call this under
371
  // normal circumstances, as the set will maintain sort order as each item
372
  // is added.
373
  sort: function (options) {
374
    var comparator = this.comparator;
375
    if (!comparator) {
376
      throw new Error(
377
        'Cannot sort a set without a comparator');
378
    }
379
    options = options || {};
380
381
    var length = comparator.length;
382
    if (_.isFunction(comparator)) {
383
      comparator = _.bind(
384
        comparator,
385
        this);
386
    }
387
388
    // Run sort based on type of `comparator`.
389
    if (length === 1 || _.isString(comparator)) {
390
      this.models = this.sortBy(comparator);
391
    } else {
392
      this.models.sort(comparator);
393
    }
394
    if (!options.silent) {
395
      this.trigger('sort', this, options);
396
    }
397
    return this;
398
  },
399
400
  // Pluck an attribute from each model in the collection.
401
  pluck: function (attr) {
402
    return this.map(attr + '');
403
  },
404
405
  // Fetch the default set of models for this collection, resetting the
406
  // collection when they arrive. If `reset: true` is passed, the response
407
  // data will be passed through the `reset` method instead of `set`.
408
  fetch: function (options) {
409
    options = _.extend({
410
      parse: true
411
    }, options);
412
    var success = options.success;
413
    var collection = this;
0 ignored issues
show
introduced by
Unexpected alias 'collection' for 'this'.
Loading history...
414
    options.success = function (resp) {
415
      var method = options.reset ? 'reset' : 'set';
416
      collection[method](resp, options);
417
      if (success) {
418
        success.call(options.context, collection,
419
          resp,
420
          options);
421
      }
422
      collection.trigger('sync', collection, resp, options);
423
    };
424
    wrapError(this, options);
425
    return this.sync('read', this, options);
426
  },
427
428
  // Create a new instance of a model in this collection. Add the model to the
429
  // collection immediately, unless `wait: true` is passed, in which case we
430
  // wait for the server to agree.
431
  create: function (model, options) {
432
    options = options ? _.clone(options) : {};
433
    var wait = options.wait;
434
    model = this._prepareModel(model, options);
435
    if (!model) {
436
      return false;
437
    }
438
    if (!wait) {
439
      this.add(model, options);
440
    }
441
    var collection = this;
0 ignored issues
show
introduced by
Unexpected alias 'collection' for 'this'.
Loading history...
442
    var success = options.success;
443
    options.success = function (m, resp, callbackOpts) {
444
      if (wait) {
445
        collection.add(m, callbackOpts);
446
      }
447
      if (success) {
448
        success.call(callbackOpts.context, m, resp,
449
          callbackOpts);
450
      }
451
    };
452
    model.save(null, options);
453
    return model;
454
  },
455
456
  // **parse** converts a response into a list of models to be added to the
457
  // collection. The default implementation is just to pass it through.
458
  parse: function (resp, options) {
0 ignored issues
show
Unused Code introduced by
The parameter options is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
459
    return resp;
460
  },
461
462
  // Create a new collection with an identical list of models as this one.
463
  clone: function () {
464
    return new this.constructor(this.models, {
465
      model: this.model,
466
      comparator: this.comparator
467
    });
468
  },
469
470
  // Define how to uniquely identify models in the collection.
471
  modelId: function (attrs) {
472
    return attrs[this.model.prototype.idAttribute || 'id'];
473
  },
474
475
  // Private method to reset all internal state. Called when the collection
476
  // is first initialized or reset.
477
  _reset: function () {
478
    this.length = 0;
479
    this.models = [];
480
    this._byId = {};
481
  },
482
483
  // Prepare a hash of attributes (or other model) to be added to this
484
  // collection.
485
  _prepareModel: function (attrs, options) {
486
    if (this._isModel(attrs)) {
487
      if (!attrs.collection) {
488
        attrs.collection = this;
489
      }
490
      return attrs;
491
    }
492
    options = options ? _.clone(options) : {};
493
    options.collection = this;
494
    var model = new this.model(attrs, options);
0 ignored issues
show
introduced by
A constructor name should not start with a lowercase letter.
Loading history...
495
    if (!model.validationError) {
496
      return model;
497
    }
498
    this.trigger('invalid', this, model.validationError, options);
499
    return false;
500
  },
501
502
  // Internal method called by both remove and set.
503
  _removeModels: function (models, options) {
504
    var removed = [];
505
    for (var i = 0; i < models.length; i++) {
506
      var model = this.get(models[i]);
507
      if (!model) {
508
        continue;
509
      }
510
511
      var index = this.indexOf(model);
512
      this.models.splice(index, 1);
513
      this.length--;
514
515
      // Remove references before triggering 'remove' event to prevent an
516
      // infinite loop. #3693
517
      delete this._byId[model.cid];
518
      var id = this.modelId(model.attributes);
519
      if (id != null) {
520
        delete this._byId[id];
521
      }
522
523
      if (!options.silent) {
524
        options.index = index;
525
        model.trigger('remove', model, this, options);
526
      }
527
528
      removed.push(model);
529
      this._removeReference(model, options);
530
    }
531
    return removed;
532
  },
533
534
  // Method for checking whether an object should be considered a model for
535
  // the purposes of adding to the collection.
536
  _isModel: function (model) {
537
    return model instanceof Model;
538
  },
539
540
  // Internal method to create a model's ties to a collection.
541
  _addReference: function (model, options) {
0 ignored issues
show
Unused Code introduced by
The parameter options is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
542
    this._byId[model.cid] = model;
543
    var id = this.modelId(model.attributes);
544
    if (id != null) {
545
      this._byId[id] = model;
546
    }
547
    model.on('all', this._onModelEvent, this);
548
  },
549
550
  // Internal method to sever a model's ties to a collection.
551
  _removeReference: function (model, options) {
0 ignored issues
show
Unused Code introduced by
The parameter options is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
552
    delete this._byId[model.cid];
553
    var id = this.modelId(model.attributes);
554
    if (id != null) {
555
      delete this._byId[id];
556
    }
557
    if (this === model.collection) {
558
      delete model.collection;
559
    }
560
    model.off('all', this._onModelEvent, this);
561
  },
562
563
  // Internal method called every time a model in the set fires an event.
564
  // Sets need to update their indexes when models change ids. All other
565
  // events simply proxy through. "add" and "remove" events that originate
566
  // in other collections are ignored.
567
  _onModelEvent: function (event, model, collection, options) {
568
    if (model) {
569
      if ((event === 'add' || event === 'remove') && collection !==
570
        this) {
571
        return;
572
      }
573
      if (event === 'destroy') {
574
        this.remove(model, options);
575
      }
576
      if (event === 'change') {
577
        var prevId = this.modelId(model.previousAttributes());
578
        var id = this.modelId(model.attributes);
579
        if (prevId !== id) {
580
          if (prevId != null) {
0 ignored issues
show
introduced by
Blocks are nested too deeply (4).
Loading history...
581
            delete this._byId[prevId];
582
          }
583
          if (id != null) {
0 ignored issues
show
introduced by
Blocks are nested too deeply (4).
Loading history...
584
            this._byId[id] = model;
585
          }
586
        }
587
      }
588
    }
589
    this.trigger.apply(this, arguments);
590
  }
591
592
});
593
594
// Underscore methods that we want to implement on the Collection.
595
// 90% of the core usefulness of Backbone Collections is actually implemented
596
// right here:
597
var collectionMethods = {
598
  forEach: 3,
599
  each: 3,
600
  map: 3,
601
  collect: 3,
602
  reduce: 0,
603
  foldl: 0,
604
  inject: 0,
605
  reduceRight: 0,
606
  foldr: 0,
607
  find: 3,
608
  detect: 3,
609
  filter: 3,
610
  select: 3,
611
  reject: 3,
612
  every: 3,
613
  all: 3,
614
  some: 3,
615
  any: 3,
616
  include: 3,
617
  includes: 3,
618
  contains: 3,
619
  invoke: 0,
620
  max: 3,
621
  min: 3,
622
  toArray: 1,
623
  size: 1,
624
  first: 3,
625
  head: 3,
626
  take: 3,
627
  initial: 3,
628
  rest: 3,
629
  tail: 3,
630
  drop: 3,
631
  last: 3,
632
  without: 0,
633
  difference: 0,
634
  indexOf: 3,
635
  shuffle: 1,
636
  lastIndexOf: 3,
637
  isEmpty: 1,
638
  chain: 1,
639
  sample: 3,
640
  partition: 3,
641
  groupBy: 3,
642
  countBy: 3,
643
  sortBy: 3,
644
  indexBy: 3,
645
  findIndex: 3,
646
  findLastIndex: 3
647
};
648
649
// Mix in each Underscore method as a proxy to `Collection#models`.
650
addUnderscoreMethods(Collection, collectionMethods, 'models');
651
652
export {
653
  Collection
654
};
655